<!--
  使用示例：
  <template>
  <div class="demo-container">
    <h2>拼图验证码演示</h2>
    <PuzzleCode
      v-model:complete="isComplete"
      :canvas-width="300"
      :canvas-height="160"
      :slider-size="40"
      :border-radius="8"
      success-text="验证成功！"
      fail-text="验证失败，请重试"
      slider-text="请拖动滑块完成验证"
      @success="handleSuccess"
      @close="handleClose"
      @fail="handleFail"
    />
    <div class="result" v-if="lastResult">
      <p>最后一次验证结果：{{ lastResult }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import PuzzleCode from '@/components/PuzzleCode.vue'

// 组件状态
const isComplete = ref(false)
const lastResult = ref('')

// 事件处理
const handleSuccess = (x) => {
  lastResult.value = `验证成功，滑动距离：${x}px`
  isComplete.value = true
  setTimeout(() => {
    isComplete.value = false // 3秒后重置状态
  }, 3000)
}

const handleFail = (x) => {
  lastResult.value = `验证失败，滑动距离：${x}px`
}

const handleClose = () => {
  lastResult.value = '验证窗口已关闭'
}
</script>

<style scoped>
.demo-container {
  padding: 20px;
  max-width: 600px;
  margin: 0 auto;
}

h2 {
  text-align: center;
  margin-bottom: 20px;
}

.result {
  margin-top: 20px;
  padding: 10px;
  border: 1px solid #eee;
  border-radius: 4px;
}
</style>

-->

<template>
  <div>
    <el-button
      v-if="complete"
      type="success"
      disabled
      plain
      style="width:100%"
    >
      已完成滑块验证
    </el-button>
    <el-button
      v-if="!complete"
      style="width:100%"
      @click="isShow = true"
    >
      点击完成滑块验证
    </el-button>

    <!-- 本体部分 -->
    <div
      :id="id"
      :class="['vue-puzzle-vcode', { show_: isShow }]"
      @mousedown="onCloseMouseDown"
      @mouseup="onCloseMouseUp"
      @touchstart="onCloseMouseDown"
      @touchend="onCloseMouseUp"
    >
      <div
        class="vue-auth-box_"
        :style="{ 'border-radius': borderRadius + 'px' }"
        @mousedown.stop
        @touchstart.stop
      >
        <div class="auth-body_" :style="`height: ${canvasHeight}px`">
          <img
            ref="img1"
            :src="image1"
            alt=""
            :width="canvasWidth"
            :height="canvasHeight"
          >
          <img
            ref="img2"
            :src="image2"
            class="auth-canvas2_"
            :height="canvasHeight"
            :style="`transform:translateX(${styleWidth - sliderBaseSize}px)`"
            alt=""
          >
          <div :class="['loading-box_', { hide_: !loading }]">
            <div class="loading-gif_">
              <span />
              <span />
              <span />
              <span />
              <span />
            </div>
          </div>
          <div :class="['info-box_', { show: infoBoxShow }, { fail: infoBoxFail }]">
            {{ infoText }}
          </div>
          <div
            :class="['flash_', { show: isSuccess }]"
            :style="`transform: translateX(${isSuccess ? `${canvasWidth + canvasHeight * 0.578}px` : `-${canvasHeight * 0.578}px`}) skew(-30deg, 0);`"
          />
          <img class="reset_" :src="resetSvg" @click="reset" />
        </div>
        <div class="auth-control_">
          <div class="range-box" :style="`height:${sliderBaseSize}px`">
            <div class="range-text">{{ sliderText }}</div>
            <div
              ref="rangeSlider"
              class="range-slider"
              :style="`width:${styleWidth}px`"
            >
              <div
                :class="['range-btn', { isDown: mouseDown }]"
                :style="`width:${sliderBaseSize}px`"
                @mousedown="onRangeMouseDown"
                @touchstart="onRangeMouseDown"
              >
                <div />
                <div />
                <div />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { captcha, captchaVerify } from '@/api/User'
import resetSvg from '@/assets/images/reset.png'

// Props
const props = defineProps({
  id: { type: String, default: undefined },
  canvasWidth: { type: Number, default: 280 },
  canvasHeight: { type: Number, default: 150 },
  sliderSize: { type: Number, default: 35 },
  successText: { type: String, default: '验证成功' },
  failText: { type: String, default: '验证失败' },
  sliderText: { type: String, default: '拖动滑块完成拼图' },
  borderRadius: { type: Number, default: 10 },
  complete: { type: Boolean, required: true, default: false },
  captcha: { type: Function, default: captcha },
  captchaVerify: { type: Function, default: captchaVerify }
})

// Emits
const emit = defineEmits(['success', 'close', 'fail', 'update:show'])

// Refs
const isShow = ref(false)
const image1 = ref(undefined)
const image2 = ref(undefined)
const mouseDown = ref(false)
const startWidth = ref(50)
const startX = ref(0)
const newX = ref(0)
const loading = ref(true)
const isCanSlide = ref(false)
const infoBoxShow = ref(false)
const infoText = ref('')
const infoBoxFail = ref(false)
const timer1 = ref(null)
const closeDown = ref(false)
const isSuccess = ref(false)
const isSubmit = ref(false)
const rangeSlider = ref(null)

// Computed
const styleWidth = computed(() => {
  const w = startWidth.value + newX.value - startX.value
  return w < sliderBaseSize.value
    ? sliderBaseSize.value
    : w > props.canvasWidth
      ? props.canvasWidth
      : w
})

const sliderBaseSize = computed(() => {
  return Math.max(
    Math.min(
      Math.round(props.sliderSize),
      Math.round(props.canvasWidth * 0.5)
    ),
    10
  )
})

// Watch
watch(() => isShow.value, (newV) => {
  if (newV) {
    document.body.classList.add('vue-puzzle-overflow')
    reset()
  } else {
    document.body.classList.remove('vue-puzzle-overflow')
  }
})

// Methods
const onClose = () => {
  if (!mouseDown.value) {
    clearTimeout(timer1.value)
    isShow.value = false
    emit('update:show', false)
    emit('close')
  }
}

const onCloseMouseDown = () => {
  closeDown.value = true
}

const onCloseMouseUp = () => {
  if (closeDown.value) {
    onClose()
  }
  closeDown.value = false
}

const onRangeMouseDown = (e) => {
  if (isCanSlide.value) {
    mouseDown.value = true
    startWidth.value = rangeSlider.value.clientWidth
    newX.value = e.clientX || e.changedTouches[0].clientX
    startX.value = e.clientX || e.changedTouches[0].clientX
  }
}

const onRangeMouseMove = (e) => {
  if (mouseDown.value) {
    e.preventDefault()
    newX.value = e.clientX || e.changedTouches[0].clientX
  }
}

const onRangeMouseUp = () => {
  if (mouseDown.value) {
    mouseDown.value = false
    submit()
  }
}

const init = () => {
  loading.value = true
  isCanSlide.value = false
  props.captcha().then(res => {
    image1.value = res.data.image1
    image2.value = res.data.image2
    loading.value = false
    isCanSlide.value = true
  })
}

const submit = () => {
  isCanSlide.value = false
  isSubmit.value = true
  const x = newX.value - startX.value
  loading.value = true
  props.captchaVerify({ x, width: props.canvasWidth }).then(res => {
    isSubmit.value = false
    loading.value = false
    if (res.data.success) {
      infoText.value = props.successText
      infoBoxFail.value = false
      infoBoxShow.value = true
      isCanSlide.value = false
      isSuccess.value = true
      clearTimeout(timer1.value)
      timer1.value = setTimeout(() => {
        isShow.value = false
        emit('update:show', false)
        emit('success', x)
      }, 800)
    } else {
      infoText.value = props.failText
      infoBoxFail.value = true
      infoBoxShow.value = true
      isCanSlide.value = false
      emit('fail', x)
      clearTimeout(timer1.value)
      timer1.value = setTimeout(() => {
        reset()
      }, 800)
    }
  })
}

const reset = () => {
  infoBoxFail.value = false
  infoBoxShow.value = false
  isCanSlide.value = true
  isSuccess.value = false
  startWidth.value = sliderBaseSize.value
  startX.value = 0
  newX.value = 0
  init()
}

// Lifecycle
onMounted(() => {
  document.body.appendChild(document.querySelector('.vue-puzzle-vcode'))
  document.addEventListener('mousemove', onRangeMouseMove, false)
  document.addEventListener('mouseup', onRangeMouseUp, false)
  document.addEventListener('touchmove', onRangeMouseMove, { passive: false })
  document.addEventListener('touchend', onRangeMouseUp, false)
  if (isShow.value) {
    document.body.classList.add('vue-puzzle-overflow')
    reset()
  }
})

onBeforeUnmount(() => {
  clearTimeout(timer1.value)
  document.body.removeChild(document.querySelector('.vue-puzzle-vcode'))
  document.removeEventListener('mousemove', onRangeMouseMove, false)
  document.removeEventListener('mouseup', onRangeMouseUp, false)
  document.removeEventListener('touchmove', onRangeMouseMove, { passive: false })
  document.removeEventListener('touchend', onRangeMouseUp, false)
})
</script>

<style lang="scss">
.vue-puzzle-vcode {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: rgba(0, 0, 0, 0.3);
  z-index: 999;
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms;

  &.show_ {
    opacity: 1;
    pointer-events: auto;
  }
}

.vue-auth-box_ {
  position: absolute;
  top: 40%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 20px;
  background: #fff;
  user-select: none;
  border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);

  .auth-body_ {
    position: relative;
    overflow: hidden;
    border-radius: 3px;

    .loading-box_ {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      background-color: rgba(0, 0, 0, 0.8);
      z-index: 20;
      opacity: 1;
      transition: opacity 200ms;
      display: flex;
      align-items: center;
      justify-content: center;

      &.hide_ {
        opacity: 0;
        pointer-events: none;

        .loading-gif_ {
          span {
            animation-play-state: paused;
          }
        }
      }

      .loading-gif_ {
        flex: none;
        height: 5px;
        line-height: 0;
        @keyframes load {
          0% {
            opacity: 1;
            transform: scale(1.3);
          }
          100% {
            opacity: 0.2;
            transform: scale(0.3);
          }
        }

        span {
          display: inline-block;
          width: 5px;
          height: 100%;
          margin-left: 2px;
          border-radius: 50%;
          background-color: #888;
          animation: load 1.04s ease infinite;

          &:nth-child(1) {
            margin-left: 0;
          }

          &:nth-child(2) {
            animation-delay: 0.13s;
          }

          &:nth-child(3) {
            animation-delay: 0.26s;
          }

          &:nth-child(4) {
            animation-delay: 0.39s;
          }

          &:nth-child(5) {
            animation-delay: 0.52s;
          }
        }
      }
    }

    .info-box_ {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      height: 24px;
      line-height: 24px;
      text-align: center;
      overflow: hidden;
      font-size: 13px;
      background-color: #83ce3f;
      opacity: 0;
      transform: translateY(24px);
      transition: all 200ms;
      color: #fff;
      z-index: 10;

      &.show {
        opacity: 0.95;
        transform: translateY(0);
      }

      &.fail {
        background-color: #ce594b;
      }
    }

    .auth-canvas2_ {
      position: absolute;
      top: 0;
      left: 0;
      z-index: 2;
    }

    .flash_ {
      position: absolute;
      top: 0;
      left: 0;
      width: 30px;
      height: 100%;
      background-color: rgba(255, 255, 255, 0.1);
      z-index: 3;

      &.show {
        transition: transform 600ms;
      }
    }

    .reset_ {
      position: absolute;
      top: 2px;
      right: 2px;
      width: 35px;
      height: auto;
      z-index: 12;
      cursor: pointer;
      transition: transform 200ms;
      transform: rotate(0deg);

      &:hover {
        transform: rotate(-90deg);
      }
    }
  }

  .auth-control_ {
    .range-box {
      position: relative;
      width: 100%;
      background-color: #eef1f8;
      margin-top: 20px;
      border-radius: 3px;
      box-shadow: 0 0 8px rgba(240, 240, 240, 0.6) inset;

      .range-text {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        font-size: 14px;
        color: #b7bcd1;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        text-align: center;
        width: 100%;
      }

      .range-slider {
        position: absolute;
        height: 100%;
        width: 50px;
        background-color: rgba(106, 160, 255, 0.8);
        border-radius: 3px;

        .range-btn {
          position: absolute;
          display: flex;
          align-items: center;
          justify-content: center;
          right: 0;
          width: 50px;
          height: 100%;
          background-color: #fff;
          border-radius: 3px;
          box-shadow: 0 0 4px #ccc;
          cursor: pointer;

          & > div {
            width: 0;
            height: 40%;
            transition: all 200ms;

            &:nth-child(2) {
              margin: 0 4px;
            }

            border: solid 1px #6aa0ff;
          }

          &:hover, &.isDown {
            & > div:first-child {
              border: solid 4px transparent;
              height: 0;
              border-right-color: #6aa0ff;
            }

            & > div:nth-child(2) {
              border-width: 3px;
              height: 0;
              border-radius: 3px;
              margin: 0 6px;
              border-right-color: #6aa0ff;
            }

            & > div:nth-child(3) {
              border: solid 4px transparent;
              height: 0;
              border-left-color: #6aa0ff;
            }
          }
        }
      }
    }
  }
}

.vue-puzzle-overflow {
  overflow: hidden !important;
}
</style>
